﻿using System;
using System.Collections.Generic;
using System.Reflection;

namespace Fadd.Components
{
	/// <summary>
	/// Creates and supplies components. 
	/// </summary>
	public class ComponentManager : IComponentProvider
	{
		private readonly Assembly _callingAssembly;
		private readonly List<Assembly> _friendAssemblies = new List<Assembly>();
		private readonly Dictionary<Type, Component> _items = new Dictionary<Type, Component>();

		/// <summary>
		/// Initializes a new instance of the <see cref="ComponentManager"/> class.
		/// </summary>
		public ComponentManager()
		{
			_callingAssembly = Assembly.GetCallingAssembly();
		}

		/// <summary>
		/// Gets or sets whether instances should be scanned if no interface type matched
		/// the requested type.
		/// </summary>
		public bool ScanInstances { get; set; }

		#region IComponentProvider Members

		/// <summary>
		/// Gets or sets where the component provider is running.
		/// </summary>
		/// <remarks>
		/// Used together with <see cref="Component.RunAt"/> and <see cref="ComponentAttribute.RunAt"/>
		/// to determine if the component should be added or not.
		/// </remarks>
		public string Location { get; set; }

		/// <summary>
		/// Get a component.
		/// </summary>
		/// <typeparam name="T">Interface type</typeparam>
		/// <returns>Component if registered, otherwise null.</returns>
		/// <remarks>
		/// Component will get created if needed.
		/// </remarks>
		/// <exception cref="CircularDependenciesException">Two components are dependent of each other, and therefore neither of them can be created.</exception>
		/// <exception cref="ConstructorMismatchException">Failed to find a suitable constructor.</exception>
		/// <exception cref="UnauthorizedAccessException">If component is internal but accessed from another assembly but it's own.</exception>
		public T Get<T>() where T : class
		{
			return (T) Get(typeof (T), Assembly.GetCallingAssembly());
		}

		/// <summary>
		/// Checks if the specified component interface have been added.
		/// </summary>
		/// <param name="interfaceType"></param>
		/// <returns>true if found; otherwise false.</returns>
		public bool Contains(Type interfaceType)
		{
			return _items.ContainsKey(interfaceType);
		}

		/// <summary>
		/// Get a component.
		/// </summary>
		/// <returns>Component if registered, otherwise null.</returns>
		/// <remarks>
		/// Component will get created if needed.
		/// </remarks>
		/// <exception cref="CircularDependenciesException">Two components are dependent of each other, and therefore neither of them can be created.</exception>
		/// <exception cref="ConstructorMismatchException">Failed to find a suitable constructor.</exception>
		/// <exception cref="UnauthorizedAccessException">If component is internal but accessed from another assembly but it's own.</exception>
		public object Get(Type interfaceType)
		{
			return Get(interfaceType, Assembly.GetCallingAssembly());
		}

		#endregion

		/// <summary>
		/// Get a component.
		/// </summary>
		/// <returns>Component if registered, otherwise null.</returns>
		/// <remarks>
		/// Component will get created if needed.
		/// </remarks>
		/// <exception cref="CircularDependenciesException">Two components are dependent of each other, and therefore neither of them can be created.</exception>
		/// <exception cref="ConstructorMismatchException">Failed to find a suitable constructor.</exception>
		/// <exception cref="UnauthorizedAccessException">If component is internal but accessed from another assembly but it's own.</exception>
		private object Get(Type interfaceType, Assembly callingAssembly)
		{
			Component component;
			lock (_items)
			{
				if (!_items.TryGetValue(interfaceType, out component))
				{
					if (ScanInstances)
					{
						// Loop through all components and match instances.
						foreach (Component value in _items.Values)
						{
							if (value.InstanceType != interfaceType) 
								continue;
							component = value;
							break;
						}
					}


					// Use event to try to find component.
					if (component == null)
					{
						var args = new ComponentMissingEventArgs(interfaceType);
						Missing(this, args);
						if (args.Instance != null)
						{
							Add(new Component
							    	{
							    		Instance = args.Instance,
							    		InstanceType = args.Instance.GetType(),
							    		InterfaceType = interfaceType,
							    		IsNotSingleton = args.IsNotSingleton
							    	});
							component = _items[interfaceType];
						}
						else
							return null;
					}
				}
			}

			if (component.IsInternal && !IsFriendAssembly(component, callingAssembly))
				throw new UnauthorizedAccessException("Component '" + component.InstanceType.FullName +
				                                      "' can only be accessed from it's own assembly ('" +
				                                      component.InstanceType.Assembly + "'). Access attempt was made from '" +
				                                      callingAssembly + "'.");

			if (component.IsNotSingleton || component.Instance == null)
				return Create(component);

			return component.Instance;
		}

		/// <summary>
		/// Visit all components.
		/// </summary>
		/// <param name="handler"></param>
		public void VisitAll(Action<Type, object> handler)
		{
			lock (_items)
			{
				foreach (Component component in _items.Values)
				{
					if (component.IsInternal && !IsFriendAssembly(component, Assembly.GetCallingAssembly()))
						continue;

					handler(component.InterfaceType, component.Instance);
				}
			}
		}

		private bool IsFriendAssembly(Component component, Assembly assembly)
		{
			return _callingAssembly.Equals(assembly)
			       || _friendAssemblies.Contains(assembly)
			       || component.InstanceType.Assembly.Equals(assembly);
		}

		/// <summary>
		/// Add custom constructor parameters for an instance type.
		/// </summary>
		/// <param name="instanceType">Instance type</param>
		/// <param name="parameters">Name, value, name, value</param>
		/// <exception cref="FormatException">Parameters should be pairs for name, value.</exception>
		/// <exception cref="InvalidOperationException">Component have not been registered yet.</exception>
		/// <example>
		/// <code>
		/// manager.AddParameters(typeof(MyComponent), "hostName", "www.gauffin.com", "port", 80);
		/// </code></example>
		public void AddParameters(Type instanceType, params object[] parameters)
		{
			if (parameters.Length%2 != 0)
				throw new FormatException("Parameters should be pairs for name, value.");

			lock (_items)
			{
				foreach (Component value in _items.Values)
				{
					if (value.InstanceType != instanceType)
						continue;

					for (int i = 0; i < parameters.Length; i += 2)
						value.KnownParameters.Add(parameters[i].ToString(), parameters[i + 1]);

					return;
				}
			}

			throw new InvalidOperationException("Component have not been registered yet: " + instanceType.FullName);
		}

		/// <summary>
		/// Add a component instance
		/// </summary>
		/// <typeparam name="T">Interface type</typeparam>
		/// <param name="instance">Instance to add</param>
		/// <exception cref="ArgumentNullException"><c>instance</c> is null.</exception>
		public void AddInstance<T>(object instance)
		{
			if (instance == null)
				throw new ArgumentNullException("instance");

			AddInstance<T>(instance, ComponentFlags.None);
		}

		/// <summary>
		/// Add a component instance
		/// </summary>
		/// <typeparam name="T">Interface type</typeparam>
		/// <param name="instance">Instance to add</param>
		/// <param name="flags">Specifies component behavior</param>
		/// <exception cref="ArgumentNullException"><c>instance</c> is null.</exception>
		public void AddInstance<T>(object instance, ComponentFlags flags)
		{
			if (instance == null)
				throw new ArgumentNullException("instance");

			Type interfaceType = typeof (T);
			var info = new Component
			           	{
			           		InstanceType = instance.GetType(),
			           		Instance = instance,
			           		InterfaceType = interfaceType,
			           		IsInternal = (flags & ComponentFlags.Internal) != 0,
			           		IsNotSingleton = (flags & ComponentFlags.NotSingleton) != 0
			           	};
			Add(info);
		}

		/// <summary>
		/// Add a component.
		/// </summary>
		/// <typeparam name="InterfaceType">Type being requested.</typeparam>
		/// <typeparam name="InstanceType">Type being created.</typeparam>
		/// <exception cref="InvalidOperationException">Type have already been mapped.</exception>
		public void Add<InterfaceType, InstanceType>()
		{
			Type interfaceType = typeof (InterfaceType);
			var componentInformation = new Component
			                           	{
			                           		InstanceType = typeof (InstanceType),
			                           		InterfaceType = interfaceType,
			                           		Instance = null
			                           	};
			Add(componentInformation);
		}

		/// <summary>
		/// Add a component.
		/// </summary>
		/// <typeparam name="InterfaceType">Type being requested.</typeparam>
		/// <typeparam name="InstanceType">Type being created.</typeparam>
		/// <exception cref="InvalidOperationException">Type have already been mapped.</exception>
		public void Add<InterfaceType, InstanceType>(ComponentFlags flags)
		{
			Type interfaceType = typeof (InterfaceType);
			var componentInformation = new Component
			                           	{
			                           		InstanceType = typeof (InstanceType),
			                           		InterfaceType = interfaceType,
			                           		Instance = null,
			                           		IsInternal = (flags & ComponentFlags.Internal) != 0,
			                           		IsNotSingleton = (flags & ComponentFlags.NotSingleton) != 0
			                           	};
			Add(componentInformation);
		}

		/// <summary>
		/// Add a component.
		/// </summary>
		/// <typeparam name="InterfaceType">Type being requested.</typeparam>
		/// <typeparam name="InstanceType">Type being created.</typeparam>
		/// <exception cref="InvalidOperationException">Type have already been mapped.</exception>
		public void Add<InterfaceType, InstanceType>(params Parameter[] parameters)
		{
			Type interfaceType = typeof (InterfaceType);
			var componentInformation = new Component
			                           	{
			                           		InstanceType = typeof (InstanceType),
			                           		InterfaceType = interfaceType,
			                           		Instance = null
			                           	};
			foreach (Parameter parameter in parameters)
				componentInformation.KnownParameters.Add(parameter.Name, parameter.Value);

			Add(componentInformation);
		}

		/// <summary>
		/// Add a component.
		/// </summary>
		/// <typeparam name="InterfaceType">Type being requested.</typeparam>
		/// <typeparam name="InstanceType">Type being created.</typeparam>
		/// <exception cref="InvalidOperationException">Type have already been mapped.</exception>
		public void Add<InterfaceType, InstanceType>(ComponentFlags flags, params Parameter[] parameters)
		{
			Type interfaceType = typeof (InterfaceType);
			var componentInformation = new Component
			                           	{
			                           		InstanceType = typeof (InstanceType),
			                           		InterfaceType = interfaceType,
			                           		Instance = null,
			                           		IsInternal = (flags & ComponentFlags.Internal) != 0,
			                           		IsNotSingleton = (flags & ComponentFlags.NotSingleton) != 0
			                           	};
			foreach (Parameter parameter in parameters)
				componentInformation.KnownParameters.Add(parameter.Name, parameter.Value);

			Add(componentInformation);
		}


		/// <summary>
		/// Adds all specified components
		/// </summary>
		/// <param name="components">The components.</param>
		/// <exception cref="ArgumentNullException"><c>components</c> is null.</exception>
		public void Add(IEnumerable<Component> components)
		{
			if (components == null)
				throw new ArgumentNullException("components");

			foreach (Component component in components)
				Add(component);
		}

		/// <summary>
		/// Add a component.
		/// </summary>
		/// <param name="information">Information about the component.</param>
		/// <exception cref="InvalidOperationException">If a component with that interface have already been added.</exception>
		/// <exception cref="ArgumentOutOfRangeException">If interface type cannot be assigned from instance type.</exception>
		/// <exception cref="ArgumentException">Interface type must be public.</exception>
		/// <exception cref="ArgumentNullException"><c>information</c> is null.</exception>
		public void Add(Component information)
		{
			if (information == null)
				throw new ArgumentNullException("information");

			// dont add components that should be running somewhere else.
			if (information.RunAt != null && string.Compare(Location, information.RunAt) != 0)
				return;

			if (!information.InterfaceType.IsAssignableFrom(information.InstanceType))
				throw new ArgumentOutOfRangeException(information.InterfaceType.FullName + " cannot be assigned from " +
				                                      information.InstanceType.FullName);
			if (!information.InterfaceType.IsPublic && !information.IsInternal)
				throw new ArgumentException("Interface '" + information.InterfaceType.FullName + "' type must be public.");

			object[] attributes = information.InstanceType.GetCustomAttributes(typeof (ComponentAttribute), true);
			if (attributes.Length > 0)
			{
				var attr = (ComponentAttribute) attributes[0];
				if (!information.IsNotSingleton)
					information.IsNotSingleton = (attr.Flags & ComponentFlags.NotSingleton) != 0;
				if (!string.IsNullOrEmpty(attr.RunAt) && string.IsNullOrEmpty(information.RunAt))
					information.RunAt = attr.RunAt;
				if (attr.Version > 0 && information.Version == 0)
					information.Version = attr.Version;
			}

			lock (_items)
			{
				if (_items.ContainsKey(information.InterfaceType))
				{
					Component storedInfo = _items[information.InterfaceType];
					if (information.Version == storedInfo.Version)
					{
						throw new InvalidOperationException("Type '" + information.InterfaceType + "' have already been mapped to '" +
						                                    information.InstanceType.FullName);
					}
					// replace with newer version.
					if (information.Version > storedInfo.Version)
					{
						if (storedInfo.IsUsed)
							throw new InvalidOperationException("An older version of '" + information.InterfaceType.FullName +
							                                    "' have already been used to create another component. You can therefore not add a newer version.");

						_items[information.InterfaceType] = information;
					}

					// ignore older versions.
					return;
				}

				_items.Add(information.InterfaceType, information);
			}
		}

		/// <summary>
		/// Create all objects.
		/// </summary>
		/// <remarks>
		/// Also singletons are created.
		/// this method is usually used to make sure that
		/// all dependencies is in place.
		/// </remarks>
		public void CreateAll()
		{
			lock (_items)
			{
				foreach (var pair in _items)
					if (pair.Value.Instance == null && !pair.Value.IsNotSingleton)
						CreateInstance(pair.Value);
			}
		}

		/// <summary>
		/// Validate that all components can be created.
		/// </summary>
		/// <exception cref="CircularDependenciesException">Two components are dependent of each other (which means that neither of them can be created).</exception>
		/// <exception cref="ConstructorMismatchException">Failed to find a suitable constructor.</exception>
		public void ValidateAll()
		{
			lock (_items)
			{
				foreach (var pair in _items)
					FindConstructor(pair.Value);
			}
		}

		/// <exception cref="ConstructorMismatchException">Failed to find a suitable constructor.</exception>
		private object Create(Component information)
		{
			if (information.Parameters == null)
				FindConstructor(information);

			return CreateInstance(information);
		}

		/// <summary>
		/// Find the constructor.
		/// </summary>
		/// <param name="component">Component to create</param>
		/// <remarks>
		/// <para>
		/// Will create all components that this one is dependent of.
		/// </para>
		/// <para>
		/// Keeps track of dependencies to avoid circular ones.
		/// </para>
		/// </remarks>
		/// <exception cref="ConstructorMismatchException">Failed to find a suitable constructor.</exception>
		/// <exception cref="CircularDependenciesException">Two components are dependent of each other, thus making it impossible to create either of them.</exception>
		protected virtual void FindConstructor(Component component)
		{
			var dependencies = new List<Component>();
			FindConstructor(component, dependencies);
		}

		/// <summary>
		/// Find the constructor.
		/// </summary>
		/// <param name="component">Component to create</param>
		/// <param name="dependencies">Linear dependency list</param>
		/// <remarks>
		/// <para>
		/// Will create all components that this one is dependent of.
		/// </para>
		/// <para>
		/// Keeps track of dependencies to avoid circular ones.
		/// </para>
		/// </remarks>
		/// <exception cref="ConstructorMismatchException">Failed to find a suitable constructor.</exception>
		/// <exception cref="CircularDependenciesException">Two components are dependent of each other, thus making it impossible to create either of them.</exception>
		protected virtual void FindConstructor(Component component, List<Component> dependencies)
		{
			// constructor have already been found.
			if (component.Parameters != null)
				return;

			if (dependencies.Contains(component))
				throw new CircularDependenciesException(dependencies);
			dependencies.Add(component);

			ConstructorInfo[] publicConstructors = component.InstanceType.GetConstructors();
			if (publicConstructors.Length == 0)
				throw new ConstructorMismatchException(component.InstanceType);

			var constructors = new List<ConstructorParameters>();

			// find a suitable constructor
			foreach (ConstructorInfo constructor in publicConstructors)
			{
				var constructorParameters = new ConstructorParameters();
				constructors.Add(constructorParameters);

				foreach (ParameterInfo parameter in constructor.GetParameters())
				{
					var constructorParameter = new ConstructorParameter {Parameter = parameter, Name = parameter.Name};
					constructorParameters.Add(constructorParameter);

					// Check specified parameters.
					foreach (var pair in component.KnownParameters)
					{
						if (string.Compare(pair.Key, parameter.Name, true) != 0) continue;
						constructorParameter.Instance = pair.Value;
						break;
					}
					if (constructorParameter.Instance != null)
						continue;

					// Check if the other parameter is a component param.
					Component typeInfo;
					bool found;
					lock (_items)
						found = _items.TryGetValue(parameter.ParameterType, out typeInfo);

					// try to found parameter using the event.
					if (!found)
					{
						var args = new ParameterRequestedEventArgs(component.InterfaceType, component.InstanceType,
						                                           parameter.ParameterType, parameter.Name);
						ParameterRequested(this, args);
						if (args.ParameterValue != null)
							constructorParameter.Instance = args.ParameterValue;

						continue;
					}

					// finally add it using the found component.
					try
					{
						constructorParameter.Instance = typeInfo;
						// make sure that our dependency have a proper constructor
						if (typeInfo.Instance == null)
							FindConstructor(typeInfo, dependencies);
						continue;
					}
					catch (ConstructorMismatchException err)
					{
						throw new ConstructorMismatchException(component.InstanceType, constructors, err);
					}
				}

				// check if all parameters was found.
				bool allFound = true;
				foreach (ConstructorParameter parameter in constructorParameters)
				{
					if (!parameter.IsMissing) continue;
					allFound = false;
					break;
				}

				if (!allFound)
					continue;

				// Add all parameters to the component info
				component.Parameters = new object[constructor.GetParameters().Length];
				int index = 0;
				foreach (ConstructorParameter parameter in constructorParameters)
					component.Parameters[index++] = parameter.Instance;

				return;
			}

			throw new ConstructorMismatchException(component.InstanceType, constructors);
		}

		/*
		/// <summary>
		/// Create a instance.
		/// </summary>
		/// <param name="information"></param>
		/// <returns>Created component</returns>
		/// <remarks>
		/// All arguments should have been found and identified before this method is called.
		/// </remarks>
		/// <exception cref="CircularDependenciesException">Two components are dependent of each other, and therefore neither of them can be created.</exception>
		protected virtual object CreateInstance(Component information)
		{
			List<Component> dependencies = new List<Component>();
			return CreateInstance(information, dependencies);
		}
		*/

		/// <summary>
		/// Creates a new instance.
		/// </summary>
		/// <param name="information">Component information.</param>
		/// <returns>Created component</returns>
		/// <remarks>
		/// Keeps track of all dependencies to make sure that there are no circular dependencies.
		/// </remarks>
		/// <exception cref="CircularDependenciesException">Two components are dependent of each other.</exception>
		protected virtual object CreateInstance(Component information)
		{
			if (information.Instance != null && !information.IsNotSingleton)
				return information.Instance;
			if (information.Parameters == null)
				FindConstructor(information);

			// FindConstructor will add the parameters or throw an exception.
			// ReSharper disable PossibleNullReferenceException
			for (int i = 0; i < information.Parameters.Length; ++i)
				// ReSharper restore PossibleNullReferenceException
			{
				object argument = information.Parameters[i];
				var info = argument as Component;
				if (info != null)
					information.Parameters[i] = CreateInstance(info);
			}

			information.Instance = Activator.CreateInstance(information.InstanceType, information.Parameters);
			// If the instance is bound to any other interface type then we'll add it there aswell
			foreach (var item in _items)
				if (item.Value.InstanceType == information.InstanceType && item.Value.Instance == null)
					item.Value.Instance = information.Instance;

			if (!information.IsInternal)
				Created(this,
				        new ComponentCreatedEventArgs(information.InterfaceType, information.Instance, information.IsNotSingleton));

			return information.Instance;
		}

		/// <summary>
		/// A component have been created.
		/// </summary>
		/// <remarks>
		/// The event is not called for internal components.
		/// </remarks>
		public event EventHandler<ComponentCreatedEventArgs> Created = delegate { };

		/// <summary>
		/// A parameter is missing for a component, this event will let you
		/// try to identify it.
		/// </summary>
		public event EventHandler<ParameterRequestedEventArgs> ParameterRequested = delegate { };

		/// <summary>
		/// A component cannot be found.
		/// </summary>
		/// <remarks>
		/// Should be used to find components from somewhere else.
		/// </remarks>
		public event EventHandler<ComponentMissingEventArgs> Missing = delegate { };

		/// <summary>
		/// Add a friend assembly. 
		/// </summary>
		/// <param name="?"></param>
		/// <remarks>
		/// Can ONLY be made by the assembly that created the <see cref="ComponentManager"/>.
		/// </remarks>
		/// <exception cref="InvalidOperationException">Friend assemblies can only be added from the assembly that instantiated the component manager.</exception>
		public void AddFriendAssembly(Assembly assembly)
		{
			if (!Assembly.GetCallingAssembly().Equals(_callingAssembly))
				throw new InvalidOperationException("Friend assemblies can only be added from: " + _callingAssembly);

			_friendAssemblies.Add(assembly);
		}
	}

	/// <summary>
	/// Generic action
	/// </summary>
	/// <typeparam name="T1"></typeparam>
	/// <typeparam name="T2"></typeparam>
	/// <param name="value1"></param>
	/// <param name="value2"></param>
	public delegate void Action<T1, T2>(T1 value1, T2 value2);
}